# encoding=utf-8
# AT收发界面布局
# BingoLee 2024-04-19
from datetime import datetime
from tkinter import filedialog
import customtkinter as ctk

from utils.kit_profile import Profile
from utils.kit_utils import Function
from viewmodel.kit_viewmodel import Topic, TopicBinder
from view.view_modules import TreeView, TextBox, PopupEntry, PopupMenu, Markdownx


class AtCmdPage(object):
    """ AT收发界面布局 """

    def __init__(self, app):
        self.app = app
        self.comport_combobox = None
        self.baudrate_combobox = None
        self.comport_switch_button = None
        self.comport_switch_state = False
        self.project_combobox = None
        self.at_cmd_tabview = None
        self.at_cmd_treeviews = {}
        self.recv_highlight_entry = None
        self.last_highlight_keyword = ''
        self.recv_textbox = None
        self.send_textbox = None
        self.project_at_cmds = {}
        self.custom_at_cmds = {}
        self.current_project = ""
        self.current_at_cmd_info = None    # (cmd_name, cmd_value)
        self.treeview_popup_menu = None

    def change_theme(self, theme):
        """ AT指令界面的主题色修改，因为treeview是ttk控件，不随ctk自动变更，因此此处手动设置 """
        for treeview in self.at_cmd_treeviews.values():
            if isinstance(treeview, TreeView):
                treeview.change_theme(theme)
        self.app.update()

    def pack(self, master):
        """ 整体布局并绑定数据变更处理回调函数 """
        ctk.CTkLabel(master, text='', image=self.app.coming_img).grid(row=0, column=0, sticky='nsew')
        master.grid_rowconfigure(0, weight=1)
        master.grid_columnconfigure(0, weight=1)

        # TODO: v0.0.1 only support log online page, return here
        return
        options_master = ctk.CTkFrame(master, fg_color='transparent')
        windows_master = ctk.CTkFrame(master, fg_color='transparent')
        options_master.grid(row=0, column=0, padx=10, pady=10, sticky='nsew')
        windows_master.grid(row=0, column=1, padx=(0, 10), pady=10, sticky='nsew')
        master.grid_rowconfigure(0, weight=1)
        master.grid_columnconfigure(0, weight=1)
        master.grid_columnconfigure(1, weight=6, minsize=850)
        self.pack_options(options_master)
        self.pack_windows(windows_master)

        Function.trigger_timer(self.update_recv_highlight_text, 1)
        TopicBinder.bind(Topic.COMPORTS_DATA_CHANGED, self.comports_data_changed_callback)
        TopicBinder.bind(Topic.SETTINGS_DATA_CHANGED, self.settings_data_changed_callback)
        TopicBinder.bind(Topic.CUSTOM_AT_CMDS_CHANGED, self.custom_at_cmd_changed_callback)
        TopicBinder.bind(Topic.PROJECT_AT_CMDS_CHANGED, self.project_at_cmd_changed_callback)
        TopicBinder.bind(Topic.AT_CMD_RECV_DATA_CHANGED, self.at_cmd_recv_data_changed_callback)

    def pack_options(self, master):
        """ 布局COM端口操作及项目对应AT指令窗口 """
        top_frame = ctk.CTkFrame(master, height=200, corner_radius=20)
        bottom_frame = ctk.CTkFrame(master, corner_radius=20)
        top_frame.grid(row=0, column=0, pady=(0, 10), sticky='ew')
        bottom_frame.grid(row=1, column=0, sticky='nsew')
        # 上半部分
        ctk.CTkLabel(top_frame, text="端口号").grid(row=0, column=0, padx=(30, 10), pady=10, sticky='w')
        self.comport_combobox = ctk.CTkComboBox(top_frame, width=180, values=(), state='readonly', border_width=1)
        self.comport_combobox.grid(row=0, column=1, padx=(10, 30), sticky='ew')
        ctk.CTkLabel(top_frame, text="波特率").grid(row=1, column=0, padx=(30, 10), pady=(0, 10), sticky='w')
        self.baudrate_combobox = ctk.CTkComboBox(top_frame, width=180, values=Profile.baudrate_list, border_width=1)
        self.baudrate_combobox.grid(row=1, column=1, padx=(10, 30), sticky='ew')
        self.comport_switch_button = ctk.CTkButton(
            top_frame, text="打开串口", font=ctk.CTkFont(size=14, weight='bold'), corner_radius=20, # border_width=1,
            command=self.switch_comport_event)
        self.comport_switch_button.grid(row=2, column=0, columnspan=2, padx=30, pady=10, sticky='ew')
        # 下半部分
        project_master = ctk.CTkFrame(bottom_frame, fg_color='transparent')
        project_master.grid(row=0, column=0, sticky='ew', padx=10, pady=(10, 0))
        ctk.CTkLabel(project_master, text='项目名').grid(row=0, column=0, padx=(20, 10))
        self.project_combobox = ctk.CTkComboBox(
            project_master, width=180, values=(), state='readonly', border_width=1, command=self.change_project_event)
        self.project_combobox.grid(row=0, column=1, padx=(10, 20), sticky='ew')
        self.at_cmd_tabview = ctk.CTkTabview(bottom_frame, corner_radius=3)
        self.at_cmd_tabview.grid(row=1, column=0, pady=(0, 10), sticky="nsew")
        master.grid_rowconfigure(1, weight=1)
        master.grid_columnconfigure(0, weight=1)
        top_frame.grid_columnconfigure(1, weight=1)
        bottom_frame.grid_columnconfigure(0, weight=1)
        bottom_frame.grid_rowconfigure(2, weight=1)
        project_master.grid_columnconfigure(1, weight=1)

    def pack_windows(self, master):
        """ 布局指令收发窗口及其按钮 """
        recv_master = ctk.CTkFrame(master, corner_radius=20)
        send_master = ctk.CTkFrame(master, corner_radius=20)
        recv_master.grid(row=0, column=0, sticky='nsew')
        send_master.grid(row=1, column=0, pady=(10, 0), sticky='ew')
        master.grid_rowconfigure(0, weight=1)
        master.grid_columnconfigure(0, weight=1)
        # recv window
        recv_top_frame = ctk.CTkFrame(recv_master, fg_color='transparent')
        recv_top_frame.grid(row=0, column=0, padx=10, pady=(10, 0), sticky='ew')
        self.recv_highlight_entry = ctk.CTkEntry(
            recv_top_frame, width=300, height=25, border_width=1, placeholder_text='输入需要高亮的文本')
        self.recv_highlight_entry.pack(side='left')
        ctk.CTkButton(
            recv_top_frame, text='清除', text_color='red', width=60, height=25, fg_color='tan', hover_color='orange',
            corner_radius=20, command=self.clear_recv_textbox_event
        ).pack(side='right')
        ctk.CTkButton(
            recv_top_frame, text='保存到文件', width=100, height=25, corner_radius=20,
            command=self.save_recv_textbox_lines_event
        ).pack(side='right', padx=10)
        self.recv_textbox = TextBox(recv_master, border_width=1, state='disabled')
        self.recv_textbox.tag_config(
            'keyword', font=ctk.CTkFont(weight='bold'), background="yellow", foreground="red", underline=1)
        self.recv_textbox.grid(row=1, column=0, padx=10, pady=(5, 10), sticky='nsew')
        recv_master.grid_rowconfigure(1, weight=1)
        recv_master.grid_columnconfigure(0, weight=1)
        # send window
        self.send_textbox = TextBox(send_master, height=50, border_width=1)
        self.send_textbox.grid(row=0, column=0, padx=10, pady=(10, 5), sticky='ew')
        send_bottom_frame = ctk.CTkFrame(send_master, fg_color='transparent')
        send_bottom_frame.grid(row=1, column=0, padx=10, pady=(0, 10), sticky='nsew')
        ctk.CTkButton(
            send_bottom_frame, text='发送', font=ctk.CTkFont(size=14, weight='bold'), width=100, height=30,
            corner_radius=20, command=self.send_at_cmd_event
        ).pack(side='left', padx=10)
        ctk.CTkButton(
            send_bottom_frame, text='清除', text_color='red', width=60, height=25, fg_color='tan', hover_color='orange',
            corner_radius=20, command=lambda: self.send_textbox.delete('0.0', 'end')
        ).pack(side='right')
        ctk.CTkButton(
            send_bottom_frame, text='保存到自定义', width=100, height=25, corner_radius=20,
            command=self.save_to_custom_at_cmd_event
        ).pack(side='right', padx=10)
        ctk.CTkButton(
            send_bottom_frame, text='刷新CRC', width=100, height=25, corner_radius=20,
            command=self.refresh_crc_event
        ).pack(side='right')
        send_master.grid_columnconfigure(0, weight=1)

    def add_tabview_for_treeview(self, tab_name, tree_values, has_menu=False):
        """ 添加一个tabview页面，即增加一个类别的指令页 """
        tab_frame = self.at_cmd_tabview.add(tab_name)
        self.app.update()
        tab_frame.grid_columnconfigure(0, weight=1)
        tab_frame.grid_rowconfigure(1, weight=1)
        treeview = TreeView(
            tab_frame, values=tree_values, is_open=True, placeholder_text='支持AT指令名称模糊搜索',
            select_callback=self.click_at_cmd_callback, doubleclick_callback=self.doubleclick_at_cmd_callback,
            rightclick_callback=self.rightclick_at_cmd_callback if has_menu else None)
        treeview.grid()
        if conflict_name := treeview.conflict_name:
            self.app.show_messagebox(('error', f"存在重复的AT指令名称:\n{conflict_name}\n请确认！", False))
        self.at_cmd_treeviews[tab_name] = treeview

    def click_at_cmd_callback(self, at_cmd_data: tuple):
        """ 单击选中指令操作 """
        cmd_name, cmd_value = at_cmd_data
        if cmd_value:
            self.current_at_cmd_info = at_cmd_data
        self.send_textbox.delete('0.0', 'end')
        self.send_textbox.insert('end', cmd_value)

    def doubleclick_at_cmd_callback(self, at_cmd_data: tuple):
        """ 双击AT指令别名操作，直接发送 """
        self.click_at_cmd_callback(at_cmd_data)
        if at_cmd_data[1]:
            self.send_at_cmd_event()

    def rightclick_at_cmd_callback(self, at_cmd_data: tuple):
        """
        AT指令treeview中右键操作回调函数
        指令标签页支持重命名及删除
        指令支持编辑及删除
        """
        def click_callback(option):
            """ 弹窗点击事件的回调函数 """
            def rename(data):
                top_entry.destroy()
                if (not data[0]) or (cmd_name == data[0]):
                    self.app.show_messagebox(('error', "AT指令别名为空或与修改前一致", False))
                    return
                TopicBinder.notify(Topic.MOD_CUSTOM_AT_CMD_TAB, (cmd_name, data[0]))  # cmd_name is tabname here

            def delete(data):
                top_entry.destroy()
                if cmd_value:
                    TopicBinder.notify(Topic.DEL_CUSTOM_AT_CMD_DATA, (parent, cmd_name, cmd_value))
                elif data[0].lower() == 'yes':
                    TopicBinder.notify(Topic.DEL_CUSTOM_AT_CMD_TAB, cmd_name)  # cmd_name is tabname when value is null

            def edit(_data):
                top_entry.destroy()
                new_name, new_value = _data
                if new_name == cmd_name and new_value == cmd_value:
                    return
                TopicBinder.notify(Topic.MOD_CUSTOM_AT_CMD_DATA,
                                   ((parent, cmd_name, cmd_value), (parent, new_name, new_value)))

            self.treeview_popup_menu.destroy()
            if option == '编辑':
                top_entry = PopupEntry(
                    title='编辑自定义AT指令', entry_values={'AT指令别名': cmd_name, 'AT指令': cmd_value},
                    confirm_callback=edit)
            elif option == '重命名':
                top_entry = PopupEntry(
                    title='重命名自定义AT指令的别名', entry_values={'新的AT指令别名': cmd_name}, confirm_callback=rename)
            elif option == '删除':
                if cmd_value:
                    top_entry = PopupEntry(
                        title='确认删除此自定义AT指令吗？', entry_values={}, confirm_callback=delete)
                else:
                    top_entry = PopupEntry(
                        title='确认删除此标签及其下的所有自定义AT指令吗？',
                        entry_values={'确认后，在此框中输入yes后删除': ''}, confirm_callback=delete)

        position, parent, cmd_name, cmd_value = at_cmd_data
        if self.treeview_popup_menu:
            self.treeview_popup_menu.destroy()
        if (not cmd_name) and (not cmd_value):
            return
        options = ('编辑', '删除') if cmd_value else ('重命名', '删除')
        self.treeview_popup_menu = PopupMenu(options, position, width=80, click_callback=click_callback)

    def save_recv_textbox_lines_event(self):
        """ 保存接收窗口的数据事件 """
        text = self.recv_textbox.get('0.0', 'end').strip()
        if not text:
            self.app.show_messagebox(('error', "接收窗口无数据！", False))
            return
        file_path = filedialog.asksaveasfilename(
            title="Save as file", filetypes=[('Log File', '.log')],
            initialfile='comport_recv.log', initialdir=Profile.root_dir)
        if not file_path:
            return
        with open(file_path, 'w+', encoding='utf-8') as f:
            f.write(text)

    def clear_recv_textbox_event(self):
        """ 清空接收窗口数据事件 """
        self.recv_textbox.configure(state='normal')
        self.recv_textbox.delete('0.0', 'end')
        self.recv_textbox.configure(state='disabled')

    def update_recv_highlight_text(self):
        """ 高亮接收框指定文本，子线程中循环处理 """
        keyword = self.recv_highlight_entry.get()
        if not keyword:
            self.recv_textbox.tag_remove("keyword", "1.0", 'end')
            self.last_highlight_keyword = ''
            return
        if self.last_highlight_keyword == keyword:
            return
        self.recv_textbox.tag_remove("keyword", "1.0", 'end')
        start_index = '1.0'
        while True:
            start_index = self.recv_textbox.search(keyword, start_index, 'end')
            if not start_index:
                break
            end_index = f"{start_index}+{len(keyword)}c"
            self.recv_textbox.tag_add("keyword", start_index, end_index)
            start_index = end_index
        self.last_highlight_keyword = keyword

    def switch_comport_event(self):
        """ 开启/关闭串口事件 """
        baudrate = self.baudrate_combobox.get().strip()
        if (not baudrate) or (not baudrate.isdigit()) or (not int(baudrate)):
            self.app.show_messagebox(('error', "波特率数据有误，请确认！", False))
            return
        if self.comport_switch_state:
            # TODO close comport
            text, color = '打开串口', 'white'
        else:
            # TODO open comport
            text, color = '关闭串口', 'yellow'
        self.comport_switch_state = not self.comport_switch_state
        self.comport_switch_button.configure(text=text, text_color=color)

    def refresh_crc_event(self):
        pass

    def save_to_custom_at_cmd_event(self):
        """ 保存自定义AT指令事件 """
        def add_custom_at_cmd(data):
            top_entry.destroy()
            if not all(data):
                self.app.show_messagebox(('error', "输入不可为空！", True))
                return
            if data[0] == data[1]:
                self.app.show_messagebox(('error', "标签名不能与指令别名一致！", True))
                return
            TopicBinder.notify(Topic.ADD_CUSTOM_AT_CMD_DATA, data)

        top_entry = PopupEntry(
            title='添加自定义AT指令', entry_values={
                'AT指令所在标签名': '', 'AT指令别名': '', 'AT指令': self.send_textbox.get('0.0', 'end').strip()
            }, confirm_callback=add_custom_at_cmd)

    @classmethod
    def format_recv_textbox_data(cls, level, time_obj, cmd_value):
        """
        为串口数据窗口格式化显示发送和接收的数据
        为保障精确度，time_obj使用datetime.datetime.now()获取
        """
        if level == 0:    # recv, error
            return f'(Recv) [%s] ᐸ==== [ %s ]\n' % (time_obj.strftime("%H:%M:%S.%f"), cmd_value)
        elif level == 1:  # recv, info
            return '\n(Recv) [%s] ᐸ==== %s\n' % (time_obj.strftime("%H:%M:%S.%f"), cmd_value)
        elif level == 2:  # send, info
            return '(Send) [%s] ====ᐳ %s' % (time_obj.strftime("%H:%M:%S.%f"), cmd_value.strip())
        else:
            return ''

    def send_at_cmd_event(self):
        """ 发送AT指令事件 """
        at_cmd_value = self.send_textbox.get('0.0', 'end').strip()
        send_str = self.format_recv_textbox_data(2, datetime.now(), at_cmd_value)

        if self.current_at_cmd_info and self.current_at_cmd_info[1] == at_cmd_value:
            at_cmd_name = self.current_at_cmd_info[0]
        else:
            at_cmd_name = '自定义'
        self.recv_textbox.configure(state='normal')
        self.recv_textbox.insert('end', '\n')
        Markdownx.render(self.recv_textbox, f'<GREY> > {at_cmd_name}')
        self.recv_textbox.insert('end', send_str)
        self.recv_textbox.see('end')
        self.recv_textbox.configure(state='disabled')
        TopicBinder.notify(Topic.SEND_AT_CMD_DATA, at_cmd_value)

    def at_cmd_recv_data_changed_callback(self, data: tuple):
        """
        AT指令接收数据事件回调函数
        更新接收窗数据
        """
        time_obj, success, txt = data
        self.recv_textbox.configure(state='normal')
        recv_str = self.format_recv_textbox_data(int(success), time_obj, txt)
        if success:
            self.recv_textbox.insert('end', recv_str)
        else:
            self.recv_textbox.insert('end', '\n')
            Markdownx.render(self.recv_textbox, f'<RED> {recv_str}')
        self.recv_textbox.configure(state='disabled')

    def comports_data_changed_callback(self, data: list):
        """
        COM端口数据变更事件回调函数
        更新COM端口控件数据
        """
        def doing():
            state = self.comport_combobox.cget('state')
            self.comport_combobox.configure(state='normal')
            self.comport_combobox.configure(values=data)
            curr = self.comport_combobox.get()
            if (not curr) and data:
                curr = data[0]
            self.comport_combobox.set(curr)
            self.comport_combobox.configure(state=state)

        # viewmodel触发回调是在子线程中进行，可能会对界面元素布局造成影响，
        # 此处在customtkinter主线程(self.after)中进行
        self.app.after(0, doing)

    def settings_data_changed_callback(self, data: dict):
        """
        用户首选项配置数据变更事件回调函数
        更新波特率控件数据
        """
        def doing():
            self.baudrate_combobox.set(data['AT_DFT_BAUDRATE'])

        self.app.after(0, doing)

    def custom_at_cmd_changed_callback(self, data: dict):
        """
        用户自定义AT指令数据变更回调函数
        更新自定义AT指令treeview界面
        """
        def doing():
            self.custom_at_cmds = data
            (tab, values), = data.items()
            del_tabs = []
            for tab_name, treeview in self.at_cmd_treeviews.items():
                if tab_name == tab:
                    self.app.after(0, lambda x=treeview: self.treeview_destroy(x))
                    self.at_cmd_tabview.delete(tab_name)
                    del_tabs.append(tab_name)
            for tab_name in del_tabs:
                del self.at_cmd_treeviews[tab_name]
            self.app.update()
            self.add_tabview_for_treeview(tab, values, has_menu=True)
            self.app.after(100, lambda: self.at_cmd_tabview.set('  自定义  '))

        self.app.after(0, doing)

    def project_at_cmd_changed_callback(self, data: dict):
        """
        项目AT指令数据变更回调函数
        更新项目AT指令treeview界面
        """
        def doing():
            self.project_at_cmds = data
            projects = sorted(list(data.keys()))
            self.project_combobox.configure(values=projects)
            project = self.project_combobox.get()
            if (not project) or (project not in projects):
                project = projects[0]
            self.project_combobox.set(project)
            self.current_project = project

            del_tabs = []
            for tab_name, treeview in self.at_cmd_treeviews.items():
                if tab_name == '  自定义  ':
                    continue
                self.app.after(0, lambda x=treeview: self.treeview_destroy(x))
                self.at_cmd_tabview.delete(tab_name)
                del_tabs.append(tab_name)
            for tab_name in del_tabs:
                del self.at_cmd_treeviews[tab_name]
            self.app.update()
            for tab_name, values in data[project].items():
                self.add_tabview_for_treeview(tab_name, values)

        self.app.after(10, doing)

    def change_project_event(self, event=None):
        """
        项目Combobox控件选择项目事件
        更新自定义AT指令及项目AT指令treeview界面
        """
        project = self.project_combobox.get()
        if self.current_project == project:
            return
        self.current_project = project
        for tab_name, treeview in self.at_cmd_treeviews.items():
            self.app.after(0, lambda x=treeview: self.treeview_destroy(x))
            self.at_cmd_tabview.delete(tab_name)
        self.app.update()
        self.at_cmd_treeviews = {}

        self.custom_at_cmd_changed_callback(self.custom_at_cmds)
        self.project_at_cmd_changed_callback(self.project_at_cmds)

    @classmethod
    def treeview_destroy(cls, treeview):
        treeview.destroy()
